use colors::{FireworkType, PixelColor};
use rand::{rngs::StdRng, Rng};
use std::{cmp, convert::TryInto, default::Default, vec::IntoIter};

use crate::renderer::colors;

#[cfg(test)]
mod tests;

#[derive(PartialEq, Copy, Clone, Debug)]
pub enum FireworkState {
  Flying,    // Just a tail - going up
  Exploding, // Bursting outwards
  Falling,   // bursting out has stopped, fall from the tips and fade from center to tips
  Fading,    // tips disappear to fading at 2x the rate fade falls
  Gone,      // nothing, a blank canvas, the inner machinations of my mind
}

impl FireworkState {
  pub fn ordinal(&self) -> u8 {
    match self {
      FireworkState::Flying => 0,
      FireworkState::Exploding => 1,
      FireworkState::Falling => 2,
      FireworkState::Fading => 3,
      FireworkState::Gone => 25,
    }
  }
}

pub trait Drawable {
  fn draw(&self) -> &[Point];
  fn clear(&self) -> &[Point];
}

pub trait Memoried {
  fn remember(&mut self);
}

#[derive(Debug)]
pub struct Firework {
  total_state: IntoIter<FireworkState>,
  center: Center,
  tail_points: TailPoints,
  exploding_points: ExplodingPoints,
  falling_points: FallingPoints,
  extinguished: bool,
  firework_type: FireworkType,
}

impl Firework {
  pub fn new(mut rng: StdRng, term_height: u16) -> Self {
    let firework_type = colors::pick_firework_type(&mut rng);
    let center = Center::new(&mut rng);
    let center_point = center.position;
    Firework {
      total_state: Firework::build_total_state(&mut rng, term_height).into_iter(),
      center,
      tail_points: TailPoints::new(center_point),
      exploding_points: Default::default(),
      falling_points: Default::default(),
      extinguished: false,
      firework_type,
    }
  }

  pub fn advance(&mut self) -> FireworkState {
    let state = self.total_state.next();
    if let Some(s) = state {
      match s {
        FireworkState::Flying => self.advance_flying(),
        FireworkState::Exploding => self.advance_exploding(),
        FireworkState::Falling => self.advance_falling(),
        FireworkState::Fading => self.advance_fading(),
        FireworkState::Gone => self.advance_gone(),
      }
      return s;
    };
    FireworkState::Gone
  }

  pub fn drawables(&self) -> [&dyn Drawable; 3] {
    [
      &self.tail_points,
      &self.exploding_points,
      &self.falling_points,
    ]
  }

  pub fn extinguished(&self) -> bool {
    self.extinguished
  }

  fn build_total_state(rng: &mut StdRng, term_height: u16) -> Vec<FireworkState> {
    let mut total_state = vec![];
    for state in vec![
      FireworkState::Flying,
      FireworkState::Exploding,
      FireworkState::Falling,
      FireworkState::Fading,
      FireworkState::Gone,
    ]
    .into_iter()
    {
      let mut advance = true;
      let mut threshold = 0;
      while advance {
        if state == FireworkState::Gone {
          // we only need one
          total_state.push(state);
          break;
        }
        if rng.gen_range(1, Firework::advance_threshold(state)) > threshold {
          total_state.push(state);
          threshold += match term_height {
            0..=24 => 11,
            25..=32 => 7,
            _ => 5,
          };
        } else {
          advance = false;
        }
      }
    }
    total_state
  }
  fn advance_flying(&mut self) {
    self.tail_points.remember();

    self.center.update_center(0, 1);
    self.tail_points.grow_tail(
      &self.center.position,
      self.firework_type.derive_color(FireworkState::Flying),
    );
  }
  fn advance_exploding(&mut self) {
    self.tail_points.remember();
    self.exploding_points.remember();

    self.tail_points.shrink_tail();
    self.exploding_points.explode(
      &self.center.position,
      self.firework_type.derive_color(FireworkState::Exploding),
    );
  }
  fn advance_falling(&mut self) {
    self.tail_points.remember();
    self.exploding_points.remember();
    self.falling_points.remember();

    self.tail_points.erase();
    self.exploding_points.collapse();
    self.falling_points.fall(
      &self.exploding_points.diagonal_tips,
      &self.exploding_points.perpendicular_tips,
      self.firework_type.derive_color(FireworkState::Falling),
    );
  }
  fn advance_fading(&mut self) {
    self.exploding_points.remember();
    self.falling_points.remember();

    self.exploding_points.erase();
    self.falling_points.fade();
  }
  fn advance_gone(&mut self) {
    self.tail_points.remember();
    self.exploding_points.remember();
    self.falling_points.remember();

    self.tail_points.erase();
    self.exploding_points.erase();
    self.falling_points.erase();

    self.extinguished = true;
  }
  fn advance_threshold(state: FireworkState) -> u64 {
    match state {
      FireworkState::Flying => 1000,
      FireworkState::Exploding => 500,
      FireworkState::Falling => 200,
      FireworkState::Fading => 200,
      FireworkState::Gone => 1,
    }
  }
}

#[derive(Debug)]
struct Center {
  position: Point,
}

impl Center {
  fn new(rng: &mut StdRng) -> Self {
    let x = rng.gen_range(0, u16::MAX);
    Center {
      position: Point {
        x,
        y: 0,
        pixel_color: Default::default(),
      },
    }
  }

  fn update_center(&mut self, x_move: i16, y_move: i16) {
    self.position = self.position.move_point(x_move, y_move)
  }
}

#[derive(Debug)]
struct TailPoints {
  tail: Vec<Point>,
  old_tail: Vec<Point>,
}

impl TailPoints {
  fn new(center: Point) -> TailPoints {
    TailPoints {
      tail: vec![center],
      old_tail: Default::default(),
    }
  }

  fn grow_tail(&mut self, center: &Point, pixel_color: PixelColor) {
    let tail_size = self.tail.len();
    self.tail.clear();
    for i in 0..tail_size + 1 {
      self.tail.push(
        center
          .move_point(0, -(i as i16))
          .with_pixel_color(pixel_color),
      )
    }
  }
  fn shrink_tail(&mut self) {
    if self.tail.len() != 1 {
      self.tail.pop();
    }
  }
  fn erase(&mut self) {
    self.tail.clear();
  }
}

impl Drawable for TailPoints {
  fn draw(&self) -> &[Point] {
    &self.tail
  }
  fn clear(&self) -> &[Point] {
    &self.old_tail
  }
}

impl Memoried for TailPoints {
  fn remember(&mut self) {
    self.old_tail = self.tail.clone();
  }
}

#[derive(Default, Debug)]
struct ExplodingPoints {
  explosion_iter: u8,
  perpendicular_tips: Vec<Point>,
  diagonal_tips: Vec<Point>,
  explosion: Vec<Point>,
  old_explosion: Vec<Point>,
}

impl ExplodingPoints {
  fn explode(&mut self, center: &Point, pixel_color: PixelColor) {
    self.explosion_iter += 1;
    self.perpendicular_tips.clear();
    self.explode_perpendicular(center, pixel_color);
    self.maybe_explode_diagonal(center, pixel_color);
  }
  fn collapse(&mut self) {
    self
      .explosion
      .drain(..(calc_drain_amt(self.explosion_iter, self.explosion.len())));
    self.explosion_iter = self.explosion_iter.saturating_sub(1);
  }
  fn erase(&mut self) {
    self.explosion.clear();
  }
  fn explode_perpendicular(&mut self, center: &Point, pixel_color: PixelColor) {
    // ↑↓←→
    let up = center
      .move_point(0, self.explosion_iter.into())
      .with_pixel_color(pixel_color);
    let down = center
      .move_point(0, -(self.explosion_iter as i16))
      .with_pixel_color(pixel_color);
    let left = center
      .move_point(self.explosion_iter as i16, 0)
      .with_pixel_color(pixel_color);
    let right = center
      .move_point(-(self.explosion_iter as i16), 0)
      .with_pixel_color(pixel_color);

    self.perpendicular_tips.push(up);
    self.perpendicular_tips.push(down);
    self.perpendicular_tips.push(left);
    self.perpendicular_tips.push(right);

    self.explosion.push(up);
    self.explosion.push(down);
    self.explosion.push(left);
    self.explosion.push(right);
  }
  fn maybe_explode_diagonal(&mut self, center: &Point, pixel_color: PixelColor) {
    if self.explosion_iter % 2 == 0 {
      self.diagonal_tips.clear();
      // diagonal points should go out half as quickly
      let move_val = self.explosion_iter / 2;
      // ⬉⬈⬋⬊
      let up_left = center
        .move_point(-(move_val as i16), move_val.into())
        .with_pixel_color(pixel_color);
      let up_right = center
        .move_point(move_val.into(), move_val.into())
        .with_pixel_color(pixel_color);
      let down_left = center
        .move_point(-(move_val as i16), -(move_val as i16))
        .with_pixel_color(pixel_color);
      let down_right = center
        .move_point(move_val.into(), -(move_val as i16))
        .with_pixel_color(pixel_color);

      self.diagonal_tips.push(up_left);
      self.diagonal_tips.push(up_right);
      self.diagonal_tips.push(down_left);
      self.diagonal_tips.push(down_right);

      self.explosion.push(up_left);
      self.explosion.push(up_right);
      self.explosion.push(down_left);
      self.explosion.push(down_right);
    }
  }
}

impl Drawable for ExplodingPoints {
  fn draw(&self) -> &[Point] {
    &self.explosion
  }
  fn clear(&self) -> &[Point] {
    &self.old_explosion
  }
}

impl Memoried for ExplodingPoints {
  fn remember(&mut self) {
    self.old_explosion = self.explosion.clone();
  }
}

#[derive(Debug, Default)]
struct FallingPoints {
  falling_iter: u8,
  falling: Vec<Point>,
  ready_for_fall: bool,
  old_falling: Vec<Point>,
  falling_tips: Vec<Point>,
}

impl FallingPoints {
  fn fall(
    &mut self,
    perpendicular_tips: &[Point],
    diagonal_tips: &[Point],
    pixel_color: PixelColor,
  ) {
    self.old_falling = self.falling.clone();
    if self.falling.is_empty() {
      self.falling_tips.extend(
        perpendicular_tips
          .iter()
          .map(|tip| tip.with_pixel_color(pixel_color)),
      );
      self.falling_tips.extend(
        diagonal_tips
          .iter()
          .map(|tip| tip.with_pixel_color(pixel_color)),
      );
    }
    self.fall_internal(perpendicular_tips, pixel_color);
    self.fall_internal(diagonal_tips, pixel_color);
  }
  fn fall_internal(&mut self, falling_tips: &[Point], pixel_color: PixelColor) {
    self.falling_iter += 1;
    falling_tips.iter().for_each(|tip| {
      self.falling.push(
        tip
          .move_point(0, -(self.falling_iter as i16))
          .with_pixel_color(pixel_color),
      )
    });
  }
  fn fade(&mut self) {
    if !self.ready_for_fall {
      self.falling_iter = self.falling_iter.saturating_add(1);
      self.ready_for_fall = true;
    }
    self
      .falling
      .drain(..calc_drain_amt(self.falling_iter, self.falling.len()));
    // perhaps fall here too
    self.falling_iter = self.falling_iter.saturating_sub(1);
  }
  fn erase(&mut self) {
    self.falling.clear();
  }
}

impl Drawable for FallingPoints {
  fn draw(&self) -> &[Point] {
    &self.falling
  }
  fn clear(&self) -> &[Point] {
    &self.old_falling
  }
}

impl Memoried for FallingPoints {
  fn remember(&mut self) {
    self.old_falling = self.falling.clone();
  }
}

#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct Point {
  pub x: u16,
  pub y: u16,
  pub pixel_color: PixelColor,
}

impl Point {
  #[must_use]
  fn move_point(&self, x_move: i16, y_move: i16) -> Point {
    Point {
      x: Point::move_single_value(self.x, x_move),
      y: Point::move_single_value(self.y, y_move),
      pixel_color: self.pixel_color,
    }
  }
  #[must_use]
  fn with_pixel_color(&self, pixel_color: PixelColor) -> Point {
    Point {
      pixel_color,
      ..*self
    }
  }
  fn move_single_value(initial: u16, move_val: i16) -> u16 {
    let fail_msg = "Illegal operation - could not convert to u16";
    if move_val > 0 {
      initial.saturating_add(move_val.abs().try_into().expect(fail_msg))
    } else {
      initial.saturating_sub(move_val.abs().try_into().expect(fail_msg))
    }
  }
}

fn calc_drain_amt(iteration: u8, drainable_len: usize) -> usize {
  cmp::min(
    match iteration {
      0..=3 => 0,
      _ => {
        if iteration % 2 == 0 {
          8
        } else {
          4
        }
      }
    },
    drainable_len,
  )
}
